package com.ds.lens.data.service;

import com.google.common.collect.Lists;
import com.ds.lens.data.common.asserts.Asserts;
import com.ds.lens.data.common.client.vo.LensMetricInVO;
import com.ds.lens.data.common.constant.AggregatorEnum;
import com.ds.lens.data.common.constant.Constants;
import com.ds.lens.data.common.query.IntervalTimestampVariable;
import com.ds.lens.data.common.query.QueryTimeStampVariable;
import com.ds.lens.data.common.util.MetricUtils;
import com.ds.lens.data.scheduler.NamedThreadFactory;
import com.ds.lens.data.service.esservice.DynamicDetectionResultService;
import com.ds.lens.data.service.metric.MetricQueryFactory;
import com.ds.lens.data.service.metric.MetricQueryService;
import com.ds.lens.data.vo.out.v2.integration.LensIntegrationDetailOutVO;
import com.ds.lens.data.vo.out.v2.metric.DynamicDetectionResultOutVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.*;

@Slf4j
@Service
public class LensQueryService {

    private static final String TIMESTAMP_FIELD = "timeStamp";
    private static final String SPLITTER = "-";
    private static final long THREE_DAY = 3 * 24 * 60 * 60 * 1000L;
    private static final IntervalTimestampVariable INTERVAL_TIMESTAMP_VARIABLE = new IntervalTimestampVariable(TIMESTAMP_FIELD);
    private static final QueryTimeStampVariable QUERY_TIME_STAMP_VARIABLE = new QueryTimeStampVariable(TIMESTAMP_FIELD);
    private ExecutorService service = new ThreadPoolExecutor(5, 10, 40, TimeUnit.SECONDS, new LinkedBlockingQueue(50), new NamedThreadFactory("MetricQuery"));

    @Autowired
    LensIntegrationService lensIntegrationService;

    @Autowired
    DynamicDetectionResultService dynamicDetectionResultService;

    @Autowired
    MetricQueryFactory metricQueryFactory;

    @Value("${dynamic.metric.query.url}")
    String dynamicMetricServiceUrl;


    public Map<String, Map<Long, Map<String, Object>>> queryAndGroup(LensMetricInVO lensMetricInVO) {
        long startTime = lensMetricInVO.getStartTime() - (lensMetricInVO.getStartTime() % (lensMetricInVO.getInterval() * 60000));
        long endTime = lensMetricInVO.getEndTime() - (lensMetricInVO.getEndTime() % (lensMetricInVO.getInterval() * 60000));
        lensMetricInVO.setStartTime(startTime);
        lensMetricInVO.setEndTime(endTime);
        Map<String, Map<Long, Map<String, Object>>> groupedMap = new HashMap<>();
        List<Map<String, Object>> results = staticQuery(lensMetricInVO);
        if (CollectionUtils.isEmpty(results)) {
            return groupedMap;
        }
        results = normalizeStaticResult(lensMetricInVO, results);
        List<String> metricBys = lensMetricInVO.getMetricBy();
        if (CollectionUtils.isEmpty(metricBys)) {
            Map<Long, Map<String, Object>> temp = new HashMap<>();
            for (Map<String, Object> map : results) {
                temp.put(Long.valueOf(map.get(TIMESTAMP_FIELD).toString()), map);
            }
            groupedMap.put("GROUP-DEFAULT", temp);
            return groupedMap;
        }

        Collections.sort(metricBys);
        for (Map<String, Object> map : results) {
            StringBuilder groupValue = new StringBuilder("GROUP-");
            for (int i = 0; i < metricBys.size(); i++) {
                groupValue.append(map.get(metricBys.get(i)));
                if (i < metricBys.size() - 1) {
                    groupValue.append("-");
                }
            }
            Map<Long, Map<String, Object>> map1 = groupedMap.get(groupValue.toString());
            if (map1 == null) {
                map1 = new LinkedHashMap<>();
                groupedMap.put(groupValue.toString(), map1);
            }
            if (map.containsKey(TIMESTAMP_FIELD)) {
                map1.put(Long.valueOf(map.get(TIMESTAMP_FIELD).toString()), map);
            }
        }
        return groupedMap;
    }

    public void markDynamicDetectionResult(List<String> detectionResultIds) {
        Asserts.assertNotEmpty(detectionResultIds, Constants.ErrorCode.PARAM_ERROR, "detectionResultIds cannot be null.");
        detectionResultIds.forEach(resultId -> dynamicDetectionResultService.markResult(resultId));
    }

    public List<DynamicDetectionResultOutVO> queryDynamicDetectionResults(Long monitorId, Integer version, Long startTime, Long endTime, List<String> metricBy) {
        Asserts.assertNotNull(monitorId, Constants.ErrorCode.PARAM_ERROR, "monitorId cannot be null.");
        Asserts.assertNotNull(version, Constants.ErrorCode.PARAM_ERROR, "version cannot be null.");
        Asserts.assertNotNull(startTime, Constants.ErrorCode.PARAM_ERROR, "startTime cannot be null.");
        Asserts.assertNotNull(endTime, Constants.ErrorCode.PARAM_ERROR, "endTime cannot be null.");
        Asserts.assertNotEmpty(metricBy, Constants.ErrorCode.PARAM_ERROR, "metricBy cannot be empty.");
        Asserts.assertTrue(Math.abs(endTime - startTime) <= THREE_DAY, Constants.ErrorCode.PARAM_ERROR, "The query time interval cannot exceed three days.");

        List<Future<List<DynamicDetectionResultOutVO>>> futures = Lists.newArrayListWithCapacity(metricBy.size());
        if (!CollectionUtils.isEmpty(metricBy)) {
            for (String str : metricBy) {
                futures.add(service.submit(() -> dynamicDetectionResultService.queryDynamicDetectionResultOutVOs(monitorId, version, startTime, endTime, str)));
            }
        }
        List<DynamicDetectionResultOutVO> results = new ArrayList<>();
        if (!CollectionUtils.isEmpty(futures)) {
            for (Future<List<DynamicDetectionResultOutVO>> future : futures) {
                try {
                    results.addAll(future.get());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    log.error("InterruptedException " + Thread.currentThread().getName(), e);
                } catch (ExecutionException e) {
                    log.error("ExecutionException " + Thread.currentThread().getName(), e);
                }
            }
        }
        return results;
    }

    public List<Map<String, Object>> query(LensMetricInVO lensMetricInVO) {
        Asserts.assertTrue(Math.abs(lensMetricInVO.getEndTime() - lensMetricInVO.getStartTime()) <= THREE_DAY, Constants.ErrorCode.PARAM_ERROR, "The query time interval cannot exceed three days.");
        long startTime = lensMetricInVO.getStartTime() - (lensMetricInVO.getStartTime() % (lensMetricInVO.getInterval() * 60000));
        long endTime = lensMetricInVO.getEndTime() - (lensMetricInVO.getEndTime() % (lensMetricInVO.getInterval() * 60000));
        lensMetricInVO.setStartTime(startTime);
        lensMetricInVO.setEndTime(endTime);
        List<Map<String, Object>> results = staticQuery(lensMetricInVO);
        return normalizeStaticResult(lensMetricInVO, results);
    }

    private List<Map<String, Object>> staticQuery(LensMetricInVO lensMetricInVO) {
        //Get integration
        LensIntegrationDetailOutVO integrationDetailOutVO = lensIntegrationService.findLensIntegrationsById(lensMetricInVO.getIntegrationId());
        Asserts.assertNotNull(integrationDetailOutVO, Constants.ErrorCode.NO_INTEGRATION_EXCEPTION);
        //Get MetricQueryService instance
        MetricQueryService metricQueryService = metricQueryFactory.getMetricQueryService(Constants.METRIC_SERVICE_PREFIX + integrationDetailOutVO.getName());
        //Call the query method
        return metricQueryService.query(lensMetricInVO, integrationDetailOutVO, INTERVAL_TIMESTAMP_VARIABLE, QUERY_TIME_STAMP_VARIABLE);
    }


    private static boolean isValidDouble(Object value) {
        if (value == null) {
            return false;
        }
        try {
            Double d = Double.valueOf(value.toString());
            return !Double.isNaN(d) && !Double.isInfinite(d);
        } catch (NumberFormatException e) {
            log.error("Invalid double " + value, e);
            return false;
        }
    }

    private List<Map<String, Object>> normalizeDynamicResult(List<Map<String, Object>> results) {
        if (CollectionUtils.isEmpty(results)) {
            return results;
        }
        for (Map<String, Object> map : results) {
            Object metricName = map.get("metricName");
            Object metricValue = map.get("metricValue");
            if (metricName != null && metricValue != null) {
                map.remove("metricName");
                map.remove("metricValue");
                map.put(metricName.toString(), metricValue);
            }
            Object timeStamp = map.get("timeStamp");
            if (timeStamp != null) {
                BigDecimal decimal = new BigDecimal(timeStamp.toString());
                map.put("timeStamp", decimal.longValue());
            }
        }
        return results;
    }

    private List<Map<String, Object>> normalizeStaticResult(LensMetricInVO inVO, List<Map<String, Object>> results) {
        if (CollectionUtils.isEmpty(results)) {
            return results;
        }
        String metricName = inVO.getMetricName() + SPLITTER + AggregatorEnum.valueOf(inVO.getMetricAggr());
        List<Map<String, Object>> list = new ArrayList<>();
        for (Map<String, Object> map : results) {
            Map<String, Object> newMap = new HashMap<>();
            boolean valid = true;
            label:
            for (String key : map.keySet()) {
                Object value = map.get(key);
                if (key.startsWith("date_histogram") || key.equals("timeStamp")) {
                    BigDecimal decimal = new BigDecimal(value.toString());
                    newMap.put("timeStamp", decimal.longValue());
                } else if (key.startsWith("tags.")) {
                    newMap.put(key.substring("tags.".length()), value);
                } else if (key.matches(".*\\(.*\\)")) {
                    boolean flag = false;
                    int index = key.indexOf('(');
                    String str = key.substring(0, index);
                    for (AggregatorEnum aggrEnum : AggregatorEnum.values()) {
                        if (aggrEnum.name().equalsIgnoreCase(str)) {
                            if (isValidDouble(value)) {
                                newMap.put(metricName, Double.valueOf(value.toString()));
                                flag = true;
                                break;
                            } else {
                                valid = false;
                                break label;
                            }
                        }
                    }
                    if (!flag) {
                        newMap.put(key, value);
                    }
                } else {
                    newMap.put(key, value);
                }
            }
            if (valid && !newMap.isEmpty()) {
                list.add(newMap);
            }
        }
        return list;
    }

    private List<Map<String, Object>> fillZero(List<Map<String, Object>> results, long startTime, long endTime,
                                               long interval, String timeStampField, String metricName, Integer aggr) {
        String metricNameWithAggr = MetricUtils.generateKey(metricName, AggregatorEnum.valueOf(aggr));
        Map<String, Object> template = results.get(0);
        List<Map<String, Object>> list = new ArrayList<>();
        int index = 0;
        for (Long i = startTime; i < endTime; i += interval, index++) {
            Map<String, Object> foundMap = null;
            for (Map<String, Object> map : results) {
                Object o = map.get(timeStampField);
                Long time = o == null ? null : Long.valueOf(o.toString());
                if (time.equals(i)) {
                    foundMap = map;
                    break;
                }
            }
            if (foundMap == null) {
                foundMap = new HashMap<>(template);
                foundMap.put(timeStampField, i);
                foundMap.put(metricNameWithAggr, 0);
            }
            list.add(foundMap);
        }
        return list;
    }
}

